﻿// Copyright (c) SimpleIdServer. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using SimpleIdServer.DPoP;
using SimpleIdServer.IdServer.Helpers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace SimpleIdServer.OpenIdConnect
{
    public class CustomOpenIdConnectHandler : RemoteAuthenticationHandler<CustomOpenIdConnectOptions>, IAuthenticationSignOutHandler
    {
        private Dictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> RealmConfigurationManagers = new Dictionary<string, ConfigurationManager<OpenIdConnectConfiguration>>();
        private Dictionary<string, OpenIdConnectConfiguration> RealmOpenidConfigurations = new Dictionary<string, OpenIdConnectConfiguration>();
        private readonly IRealmStore _realmStore;
        private const string NonceProperty = "N";
        private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
        private const string PushedAuthorizationRequestEndpoint = "pushed_authorization_request_endpoint";
        private const string RequirePushedAuthorizationRequests = "require_pushed_authorization_requests";
        private const string MtlsEndpointAliasesName = "mtls_endpoint_aliases";
        private const string RequestParameterName = "request";
        private const string ClientIdParameterName = "client_id";
        private const string RequestUriParameterName = "request_uri";
        private const string ResponseParameterName = "response";
        private const string TokenEndpointParameterName = "token_endpoint";
        private static IEnumerable<string> jwtResponseModes = new List<string>
        {
            "jwt", "query.jwt", "fragment.jwt", "form_post.jwt"
        };
        private SecurityKey _dPoPSecurityKey;
        private DateTime? _lastDPoPSecurityKeyRotationDateTime = null;
        private OpenIdConnectConfiguration? _configuration;
        protected HttpClient Backchannel => Options.Backchannel;
        protected new OpenIdConnectEvents Events
        {
            get { return (OpenIdConnectEvents)base.Events; }
            set { base.Events = value; }
        }

        protected record AuthorizationExtractionResult
        {
            public bool IsValid { get; set; }
            public HandleRequestResult ErrorResult { get; set; }
            public OpenIdConnectMessage OpenIdConnectMessage { get; set; }
        }

        public CustomOpenIdConnectHandler(IRealmStore realmStore, IOptionsMonitor<CustomOpenIdConnectOptions> options, ILoggerFactory logger, HtmlEncoder htmlEncoder, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
        {
            _realmStore = realmStore;
        }

        public override Task<bool> ShouldHandleRequestAsync()
        {
            if (Options.IsRealmEnabled)
            {
                return Task.FromResult($"/{_realmStore.Realm}{Options.CallbackPath}" == Request.Path);
            }

            return Task.FromResult(Options.CallbackPath == Request.Path);
        }

        public override Task<bool> HandleRequestAsync()
        {
            if (Options.RemoteSignOutPath.HasValue && Options.RemoteSignOutPath == Request.Path)
            {
                return HandleRemoteSignOutAsync();
            }
            else if (Options.SignedOutCallbackPath.HasValue && Options.SignedOutCallbackPath == Request.Path)
            {
                return HandleSignOutCallbackAsync();
            }

            return base.HandleRequestAsync();
        }

        /// <inheritdoc />
        protected virtual async Task<bool> HandleRemoteSignOutAsync()
        {
            OpenIdConnectMessage? message = null;

            if (HttpMethods.IsGet(Request.Method))
            {
                // ToArray handles the StringValues.IsNullOrEmpty case. We assume non-empty Value does not contain null elements.
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
                message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value.ToArray())));
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
            }

            // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
            else if (HttpMethods.IsPost(Request.Method)
              && !string.IsNullOrEmpty(Request.ContentType)
              // May have media/type; charset=utf-8, allow partial match.
              && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
              && Request.Body.CanRead)
            {
                var form = await Request.ReadFormAsync(Context.RequestAborted);

                // ToArray handles the StringValues.IsNullOrEmpty case. We assume non-empty Value does not contain null elements.
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
                message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value.ToArray())));
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
            }

            var remoteSignOutContext = new RemoteSignOutContext(Context, Scheme, Options, message);
            await Events.RemoteSignOut(remoteSignOutContext);

            if (remoteSignOutContext.Result != null)
            {
                if (remoteSignOutContext.Result.Handled)
                {
                    Logger.RemoteSignOutHandledResponse();
                    return true;
                }
                if (remoteSignOutContext.Result.Skipped)
                {
                    Logger.RemoteSignOutSkipped();
                    return false;
                }
                if (remoteSignOutContext.Result.Failure != null)
                {
                    throw new InvalidOperationException("An error was returned from the RemoteSignOut event.", remoteSignOutContext.Result.Failure);
                }
            }

            if (message == null)
            {
                return false;
            }

            // Try to extract the session identifier from the authentication ticket persisted by the sign-in handler.
            // If the identifier cannot be found, bypass the session identifier checks: this may indicate that the
            // authentication cookie was already cleared, that the session identifier was lost because of a lossy
            // external/application cookie conversion or that the identity provider doesn't support sessions.
            var principal = (await Context.AuthenticateAsync(Options.SignOutScheme))?.Principal;

            var sid = principal?.FindFirst(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Sid)?.Value;
            if (!string.IsNullOrEmpty(sid))
            {
                // Ensure a 'sid' parameter was sent by the identity provider.
                if (string.IsNullOrEmpty(message.Sid))
                {
                    Logger.RemoteSignOutSessionIdMissing();
                    return true;
                }
                // Ensure the 'sid' parameter corresponds to the 'sid' stored in the authentication ticket.
                if (!string.Equals(sid, message.Sid, StringComparison.Ordinal))
                {
                    Logger.RemoteSignOutSessionIdInvalid();
                    return true;
                }
            }

            var iss = principal?.FindFirst(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Iss)?.Value;
            if (!string.IsNullOrEmpty(iss))
            {
                // Ensure a 'iss' parameter was sent by the identity provider.
                if (string.IsNullOrEmpty(message.Iss))
                {
                    Logger.RemoteSignOutIssuerMissing();
                    return true;
                }
                // Ensure the 'iss' parameter corresponds to the 'iss' stored in the authentication ticket.
                if (!string.Equals(iss, message.Iss, StringComparison.Ordinal))
                {
                    Logger.RemoteSignOutIssuerInvalid();
                    return true;
                }
            }

            Logger.RemoteSignOut();

            // We've received a remote sign-out request
            await Context.SignOutAsync(Options.SignOutScheme);
            return true;
        }

        /// <summary>
        /// Response to the callback from OpenId provider after session ended.
        /// </summary>
        /// <returns>A task executing the callback procedure</returns>
        protected virtual async Task<bool> HandleSignOutCallbackAsync()
        {
            // ToArray handles the StringValues.IsNullOrEmpty case. We assume non-empty Value does not contain null elements.
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
            var message = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value.ToArray())));
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.

            AuthenticationProperties? properties = null;
            if (!string.IsNullOrEmpty(message.State))
            {
                properties = Options.StateDataFormat.Unprotect(message.State);
            }

            var signOut = new RemoteSignOutContext(Context, Scheme, Options, message)
            {
                Properties = properties,
            };

            await Events.SignedOutCallbackRedirect(signOut);
            if (signOut.Result != null)
            {
                if (signOut.Result.Handled)
                {
                    Logger.SignOutCallbackRedirectHandledResponse();
                    return true;
                }
                if (signOut.Result.Skipped)
                {
                    Logger.SignOutCallbackRedirectSkipped();
                    return false;
                }
                if (signOut.Result.Failure != null)
                {
                    throw new InvalidOperationException("An error was returned from the SignedOutCallbackRedirect event.", signOut.Result.Failure);
                }
            }

            properties = signOut.Properties;
            if (!string.IsNullOrEmpty(properties?.RedirectUri))
            {
                Response.Redirect(properties.RedirectUri);
            }

            return true;
        }

        public async Task SignOutAsync(AuthenticationProperties properties)
        {
            var target = ResolveTarget(Options.ForwardSignOut);
            if (target != null)
            {
                await Context.SignOutAsync(target, properties);
                return;
            }

            properties ??= new AuthenticationProperties();

            Logger.EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(GetType().FullName!);

            var configuration = await GetConfiguration();
            var message = new OpenIdConnectMessage()
            {
                EnableTelemetryParameters = !Options.DisableTelemetry,
                IssuerAddress = configuration?.EndSessionEndpoint ?? string.Empty,

                // Redirect back to SigneOutCallbackPath first before user agent is redirected to actual post logout redirect uri
                PostLogoutRedirectUri = BuildRedirectUriIfRelative(Options.SignedOutCallbackPath)
            };

            // Get the post redirect URI.
            if (string.IsNullOrEmpty(properties.RedirectUri))
            {
                properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri);
                if (string.IsNullOrWhiteSpace(properties.RedirectUri))
                {
                    properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
                }
            }
            Logger.PostSignOutRedirect(properties.RedirectUri);

            // Attach the identity token to the logout request when possible.
            message.IdTokenHint = await Context.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken);

            var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
            {
                ProtocolMessage = message
            };

            await Events.RedirectToIdentityProviderForSignOut(redirectContext);
            if (redirectContext.Handled)
            {
                Logger.RedirectToIdentityProviderForSignOutHandledResponse();
                return;
            }

            message = redirectContext.ProtocolMessage;

            if (!string.IsNullOrEmpty(message.State))
            {
                properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
            }

            message.State = Options.StateDataFormat.Protect(properties);

            if (string.IsNullOrEmpty(message.IssuerAddress))
            {
                throw new InvalidOperationException("Cannot redirect to the end session endpoint, the configuration may be missing or invalid.");
            }

            if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
            {
                var redirectUri = message.CreateLogoutRequestUrl();
                if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
                {
                    Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri);
                }

                Response.Redirect(redirectUri);
            }
            else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
            {
                var content = message.BuildFormPost();
                var buffer = Encoding.UTF8.GetBytes(content);

                Response.ContentLength = buffer.Length;
                Response.ContentType = "text/html;charset=UTF-8";

                // Emit Cache-Control=no-cache to prevent client caching.
                Response.Headers.CacheControl = "no-cache, no-store";
                Response.Headers.Pragma = "no-cache";
                Response.Headers.Expires = HeaderValueEpocDate;

                await Response.Body.WriteAsync(buffer);
            }
            else
            {
                throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
            }

            Logger.AuthenticationSchemeSignedOut(Scheme.Name);
        }

        protected async override Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
        {
            Logger.EnteringOpenIdAuthenticationHandlerHandleRemoteAuthenticateAsync(GetType().FullName!);
            OpenIdConnectMessage? authorizationResponse = null;

            if (HttpMethods.IsGet(Request.Method))
            {
                var authResponse = await ExtractAuthorizationResponse(Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value.ToArray())));
                if (!authResponse.IsValid)
                    return authResponse.ErrorResult;
                // ToArray handles the StringValues.IsNullOrEmpty case. We assume non-empty Value does not contain null elements.
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
                authorizationResponse = authResponse.OpenIdConnectMessage;
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.

                // response_mode=query (explicit or not) and a response_type containing id_token
                // or token are not considered as a safe combination and MUST be rejected.
                // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security
                if (!string.IsNullOrEmpty(authorizationResponse.IdToken) || !string.IsNullOrEmpty(authorizationResponse.AccessToken))
                {
                    if (Options.SkipUnrecognizedRequests)
                    {
                        // Not for us?
                        return HandleRequestResult.SkipHandler();
                    }
                    return CustomHandlerRequestResults.UnexpectedParams;
                }
            }
            // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small.
            else if (HttpMethods.IsPost(Request.Method)
              && !string.IsNullOrEmpty(Request.ContentType)
              // May have media/type; charset=utf-8, allow partial match.
              && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
              && Request.Body.CanRead)
            {
                var form = await Request.ReadFormAsync(Context.RequestAborted);
                var authResponse = await ExtractAuthorizationResponse(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value.ToArray())));
                if (!authResponse.IsValid)
                    return authResponse.ErrorResult;
                // ToArray handles the StringValues.IsNullOrEmpty case. We assume non-empty Value does not contain null elements.
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
                authorizationResponse = authResponse.OpenIdConnectMessage;
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
            }

            if (authorizationResponse == null)
            {
                if (Options.SkipUnrecognizedRequests)
                {
                    // Not for us?
                    return HandleRequestResult.SkipHandler();
                }
                return CustomHandlerRequestResults.NoMessage;
            }

            AuthenticationProperties? properties = null;
            try
            {
                properties = ReadPropertiesAndClearState(authorizationResponse);

                var messageReceivedContext = await RunMessageReceivedEventAsync(authorizationResponse, properties);
                if (messageReceivedContext.Result != null)
                {
                    return messageReceivedContext.Result;
                }
                authorizationResponse = messageReceivedContext.ProtocolMessage;
                properties = messageReceivedContext.Properties;

                if (properties == null || properties.Items.Count == 0)
                {
                    // Fail if state is missing, it's required for the correlation id.
                    if (string.IsNullOrEmpty(authorizationResponse.State))
                    {
                        // This wasn't a valid OIDC message, it may not have been intended for us.
                        Logger.NullOrEmptyAuthorizationResponseState();
                        if (Options.SkipUnrecognizedRequests)
                        {
                            return HandleRequestResult.SkipHandler();
                        }
                        return HandleRequestResult.Fail(Resources.MessageStateIsNullOrEmpty);
                    }

                    properties = ReadPropertiesAndClearState(authorizationResponse);
                }

                if (properties == null)
                {
                    Logger.UnableToReadAuthorizationResponseState();
                    if (Options.SkipUnrecognizedRequests)
                    {
                        // Not for us?
                        return HandleRequestResult.SkipHandler();
                    }

                    // if state exists and we failed to 'unprotect' this is not a message we should process.
                    return HandleRequestResult.Fail(Resources.MessageStateIsInvalid);
                }

                if (!ValidateCorrelationId(properties))
                {
                    return HandleRequestResult.Fail("Correlation failed.", properties);
                }

                // if any of the error fields are set, throw error null
                if (!string.IsNullOrEmpty(authorizationResponse.Error))
                {
                    // Note: access_denied errors are special protocol errors indicating the user didn't
                    // approve the authorization demand requested by the remote authorization server.
                    // Since it's a frequent scenario (that is not caused by incorrect configuration),
                    // denied errors are handled differently using HandleAccessDeniedErrorAsync().
                    // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
                    if (string.Equals(authorizationResponse.Error, "access_denied", StringComparison.Ordinal))
                    {
                        var result = await HandleAccessDeniedErrorAsync(properties);
                        if (!result.None)
                        {
                            return result;
                        }
                    }

                    return HandleRequestResult.Fail(CreateOpenIdConnectProtocolException(authorizationResponse, response: null), properties);
                }


                var configuration = await GetConfiguration();

                PopulateSessionProperties(authorizationResponse, properties, configuration);

                ClaimsPrincipal? user = null;
                JwtSecurityToken? jwt = null;
                string? nonce = null;
                var validationParameters = Options.TokenValidationParameters.Clone();

                // Hybrid or Implicit flow
                if (!string.IsNullOrEmpty(authorizationResponse.IdToken))
                {
                    Logger.ReceivedIdToken();
                    user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, configuration, out jwt);

                    nonce = jwt.Payload.Nonce;
                    if (!string.IsNullOrEmpty(nonce))
                    {
                        nonce = ReadNonceCookie(nonce);
                    }

                    var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, null, user, properties, jwt, nonce);
                    if (tokenValidatedContext.Result != null)
                    {
                        return tokenValidatedContext.Result;
                    }
                    authorizationResponse = tokenValidatedContext.ProtocolMessage;
                    user = tokenValidatedContext.Principal;
                    properties = tokenValidatedContext.Properties;
                    jwt = tokenValidatedContext.SecurityToken;
                    nonce = tokenValidatedContext.Nonce;
                }

                Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext()
                {
                    ClientId = Options.ClientId,
                    ProtocolMessage = authorizationResponse,
                    ValidatedIdToken = jwt,
                    Nonce = nonce
                });

                OpenIdConnectMessage? tokenEndpointResponse = null;

                // Authorization Code or Hybrid flow
                if (!string.IsNullOrEmpty(authorizationResponse.Code))
                {
                    var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, user, properties!, jwt);
                    if (authorizationCodeReceivedContext.Result != null)
                    {
                        return authorizationCodeReceivedContext.Result;
                    }
                    authorizationResponse = authorizationCodeReceivedContext.ProtocolMessage;
                    user = authorizationCodeReceivedContext.Principal!;
                    properties = authorizationCodeReceivedContext.Properties!;
                    var tokenEndpointRequest = authorizationCodeReceivedContext.TokenEndpointRequest;
                    // If the developer redeemed the code themselves...
                    tokenEndpointResponse = authorizationCodeReceivedContext.TokenEndpointResponse;
                    jwt = authorizationCodeReceivedContext.JwtSecurityToken!;

                    if (!authorizationCodeReceivedContext.HandledCodeRedemption)
                    {
                        tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest!, configuration: configuration);
                    }

                    var tokenResponseReceivedContext = await RunTokenResponseReceivedEventAsync(authorizationResponse, tokenEndpointResponse!, user, properties);
                    if (tokenResponseReceivedContext.Result != null)
                    {
                        return tokenResponseReceivedContext.Result;
                    }

                    authorizationResponse = tokenResponseReceivedContext.ProtocolMessage;
                    tokenEndpointResponse = tokenResponseReceivedContext.TokenEndpointResponse;
                    user = tokenResponseReceivedContext.Principal;
                    properties = tokenResponseReceivedContext.Properties!;

                    // no need to validate signature when token is received using "code flow" as per spec
                    // [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation].
                    validationParameters.RequireSignedTokens = false;

                    // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response.
                    // And we'll want to validate the new JWT in ValidateTokenResponse.
                    var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, configuration, out var tokenEndpointJwt);

                    // Avoid reading & deleting the nonce cookie, running the event, etc, if it was already done as part of the authorization response validation.
                    if (user == null)
                    {
                        nonce = tokenEndpointJwt.Payload.Nonce;
                        if (!string.IsNullOrEmpty(nonce))
                        {
                            nonce = ReadNonceCookie(nonce);
                        }

                        var tokenValidatedContext = await RunTokenValidatedEventAsync(authorizationResponse, tokenEndpointResponse, tokenEndpointUser, properties, tokenEndpointJwt, nonce);
                        if (tokenValidatedContext.Result != null)
                        {
                            return tokenValidatedContext.Result;
                        }

                        authorizationResponse = tokenValidatedContext.ProtocolMessage;
                        tokenEndpointResponse = tokenValidatedContext.TokenEndpointResponse;
                        user = tokenValidatedContext.Principal!;
                        properties = tokenValidatedContext.Properties;
                        jwt = tokenValidatedContext.SecurityToken;
                        nonce = tokenValidatedContext.Nonce;
                    }
                    else
                    {
                        if (!string.Equals(jwt.Subject, tokenEndpointJwt.Subject, StringComparison.Ordinal))
                        {
                            throw new SecurityTokenException("The sub claim does not match in the id_token's from the authorization and token endpoints.");
                        }

                        jwt = tokenEndpointJwt;
                    }

                    // Validate the token response if it wasn't provided manually
                    if (!authorizationCodeReceivedContext.HandledCodeRedemption)
                    {
                        Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext()
                        {
                            ClientId = Options.ClientId,
                            ProtocolMessage = tokenEndpointResponse,
                            ValidatedIdToken = jwt,
                            Nonce = nonce
                        });
                    }
                }

                if (Options.SaveTokens)
                {
                    SaveTokens(properties!, tokenEndpointResponse ?? authorizationResponse);
                }

                if (Options.GetClaimsFromUserInfoEndpoint)
                {
                    return await GetUserInformationAsync(tokenEndpointResponse ?? authorizationResponse, jwt!, user!, properties!, configuration);
                }
                else
                {
                    using (var payload = JsonDocument.Parse("{}"))
                    {
                        var identity = (ClaimsIdentity)user!.Identity!;
                        foreach (var action in Options.ClaimActions)
                        {
                            action.Run(payload.RootElement, identity, ClaimsIssuer);
                        }
                    }
                }

                return HandleRequestResult.Success(new AuthenticationTicket(user, properties, Scheme.Name));
            }
            catch (Exception exception)
            {
                Logger.ExceptionProcessingMessage(exception);

                // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the event.
                if (Options.RefreshOnIssuerKeyNotFound && exception is SecurityTokenSignatureKeyNotFoundException)
                {
                    if (Options.ConfigurationManager != null)
                    {
                        Logger.ConfigurationManagerRequestRefreshCalled();
                        Options.ConfigurationManager.RequestRefresh();
                    }
                }

                var authenticationFailedContext = await RunAuthenticationFailedEventAsync(authorizationResponse, exception);
                if (authenticationFailedContext.Result != null)
                {
                    return authenticationFailedContext.Result;
                }

                return HandleRequestResult.Fail(exception, properties);
            }
        }

        protected async Task<AuthorizationExtractionResult> ExtractAuthorizationResponse(IEnumerable<KeyValuePair<string, string[]>> dic)
        {
            if (jwtResponseModes.Contains(Options.ResponseMode))
            {
                if (!dic.Any(kvp => kvp.Key == ResponseParameterName))
                    return new AuthorizationExtractionResult { IsValid = false, ErrorResult = CustomHandlerRequestResults.NoResponse };
                var response = dic.First(kvp => kvp.Key == ResponseParameterName).Value[0];
                var handler = new JsonWebTokenHandler();
                var payload = handler.ReadJsonWebToken(response);
                var configuration = await GetConfiguration();

                var jsonWebKey = configuration.JsonWebKeySet.Keys.FirstOrDefault(k => k.KeyId == payload.Kid);
                if (jsonWebKey == null)
                    return new AuthorizationExtractionResult { IsValid = false, ErrorResult = CustomHandlerRequestResults.UnknownJWK };
                var validationResult = new JsonWebTokenHandler().ValidateToken(response, new TokenValidationParameters
                {
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidateLifetime = false,
                    IssuerSigningKey = jsonWebKey
                });
                if (!validationResult.IsValid)
                    return new AuthorizationExtractionResult { IsValid = false, ErrorResult = CustomHandlerRequestResults.ResponseSignatureIsInvalid };
                return new AuthorizationExtractionResult { IsValid = true, OpenIdConnectMessage = new OpenIdConnectMessage(validationResult.Claims.Select(c => new KeyValuePair<string, string[]>(c.Key, new string[] { c.Value.ToString() }))) };
            }

            return new AuthorizationExtractionResult { IsValid = true, OpenIdConnectMessage = new OpenIdConnectMessage(dic) };
        }

        /// <summary>
        /// Goes to UserInfo endpoint to retrieve additional claims and add any unique claims to the given identity.
        /// </summary>
        /// <param name="message">message that is being processed</param>
        /// <param name="jwt">The <see cref="JwtSecurityToken"/>.</param>
        /// <param name="principal">The claims principal and identities.</param>
        /// <param name="properties">The authentication properties.</param>
        /// <returns><see cref="HandleRequestResult"/> which is used to determine if the remote authentication was successful.</returns>
        protected virtual async Task<HandleRequestResult> GetUserInformationAsync(
            OpenIdConnectMessage message, JwtSecurityToken jwt,
            ClaimsPrincipal principal, AuthenticationProperties properties,
            OpenIdConnectConfiguration configuration)
        {
            var userInfoEndpoint = configuration?.UserInfoEndpoint;

            if (string.IsNullOrEmpty(userInfoEndpoint))
            {
                Logger.UserInfoEndpointNotSet();
                return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name));
            }
            if (string.IsNullOrEmpty(message.AccessToken))
            {
                Logger.AccessTokenNotAvailable();
                return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name));
            }
            Logger.RetrievingClaims();
            var requestMessage = new HttpRequestMessage(HttpMethod.Get, userInfoEndpoint);
            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", message.AccessToken);
            requestMessage.Version = Backchannel.DefaultRequestVersion;
            var responseMessage = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
            responseMessage.EnsureSuccessStatusCode();
            var userInfoResponse = await responseMessage.Content.ReadAsStringAsync(Context.RequestAborted);

            JsonDocument user;
            var contentType = responseMessage.Content.Headers.ContentType;
            if (contentType?.MediaType?.Equals("application/json", StringComparison.OrdinalIgnoreCase) ?? false)
            {
                user = JsonDocument.Parse(userInfoResponse);
            }
            else if (contentType?.MediaType?.Equals("application/jwt", StringComparison.OrdinalIgnoreCase) ?? false)
            {
                var userInfoEndpointJwt = new JwtSecurityToken(userInfoResponse);
                user = JsonDocument.Parse(userInfoEndpointJwt.Payload.SerializeToJson());
            }
            else
            {
                return HandleRequestResult.Fail("Unknown response type: " + contentType?.MediaType, properties);
            }

            using (user)
            {
                var userInformationReceivedContext = await RunUserInformationReceivedEventAsync(principal, properties, message, user);
                if (userInformationReceivedContext.Result != null)
                {
                    return userInformationReceivedContext.Result;
                }
                principal = userInformationReceivedContext.Principal!;
                properties = userInformationReceivedContext.Properties!;
                using (var updatedUser = userInformationReceivedContext.User)
                {
                    Options.ProtocolValidator.ValidateUserInfoResponse(new OpenIdConnectProtocolValidationContext()
                    {
                        UserInfoEndpointResponse = userInfoResponse,
                        ValidatedIdToken = jwt,
                    });

                    var identity = (ClaimsIdentity)principal.Identity!;

                    foreach (var action in Options.ClaimActions)
                    {
                        action.Run(updatedUser.RootElement, identity, ClaimsIssuer);
                    }
                }
            }

            return HandleRequestResult.Success(new AuthenticationTicket(principal, properties, Scheme.Name));
        }

        protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            await HandleChallengeAsyncInternal(properties);
            var location = Context.Response.Headers.Location;
            if (location == StringValues.Empty)
            {
                location = "(not set)";
            }

            var cookie = Context.Response.Headers.SetCookie;
            if (cookie == StringValues.Empty)
            {
                cookie = "(not set)";
            }

            Logger.HandleChallenge(location.ToString(), cookie.ToString());
        }

        /// <summary>
        /// Save the tokens contained in the <see cref="OpenIdConnectMessage"/> in the <see cref="ClaimsPrincipal"/>.
        /// </summary>
        /// <param name="properties">The <see cref="AuthenticationProperties"/> in which tokens are saved.</param>
        /// <param name="message">The OpenID Connect response.</param>
        private void SaveTokens(AuthenticationProperties properties, OpenIdConnectMessage message)
        {
            var tokens = new List<AuthenticationToken>();

            if (!string.IsNullOrEmpty(message.AccessToken))
            {
                tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = message.AccessToken });
            }

            if (!string.IsNullOrEmpty(message.IdToken))
            {
                tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.IdToken, Value = message.IdToken });
            }

            if (!string.IsNullOrEmpty(message.RefreshToken))
            {
                tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.RefreshToken, Value = message.RefreshToken });
            }

            if (!string.IsNullOrEmpty(message.TokenType))
            {
                tokens.Add(new AuthenticationToken { Name = OpenIdConnectParameterNames.TokenType, Value = message.TokenType });
            }

            if (!string.IsNullOrEmpty(message.ExpiresIn))
            {
                if (int.TryParse(message.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
                {
                    var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
                    // https://www.w3.org/TR/xmlschema-2/#dateTime
                    // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
                    tokens.Add(new AuthenticationToken { Name = "expires_at", Value = expiresAt.ToString("o", CultureInfo.InvariantCulture) });
                }
            }

            properties.StoreTokens(tokens);
        }

        private AuthenticationProperties? ReadPropertiesAndClearState(OpenIdConnectMessage message)
        {
            AuthenticationProperties? properties = null;
            if (!string.IsNullOrEmpty(message.State))
            {
                properties = Options.StateDataFormat.Unprotect(message.State);

                if (properties != null)
                {
                    // If properties can be decoded from state, clear the message state.
                    properties.Items.TryGetValue(OpenIdConnectDefaults.UserstatePropertiesKey, out var userstate);
                    message.State = userstate;
                }
            }
            return properties;
        }

        private async Task HandleChallengeAsyncInternal(AuthenticationProperties properties)
        {
            Logger.EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(GetType().FullName!);

            // order for local RedirectUri
            // 1. challenge.Properties.RedirectUri
            // 2. CurrentUri if RedirectUri is not set)
            if (string.IsNullOrEmpty(properties.RedirectUri))
            {
                properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
            }
            Logger.PostAuthenticationLocalRedirect(properties.RedirectUri);

            var configuration = await GetConfiguration();
            CheckOptions(configuration);
            var message = new OpenIdConnectMessage
            {
                ClientId = Options.ClientId,
                EnableTelemetryParameters = !Options.DisableTelemetry,
                IssuerAddress = configuration?.AuthorizationEndpoint ?? string.Empty,
                RedirectUri = properties.GetParameter<string>(OpenIdConnectParameterNames.RedirectUri) ?? BuildRedirectUri(Options.IsRealmEnabled ? $"/{_realmStore.Realm}{Options.CallbackPath}" : Options.CallbackPath),
                Resource = Options.Resource,
                ResponseType = Options.ResponseType,
                Prompt = properties.GetParameter<string>(OpenIdConnectParameterNames.Prompt) ?? Options.Prompt,
                Scope = string.Join(" ", properties.GetParameter<ICollection<string>>(OpenIdConnectParameterNames.Scope) ?? Options.Scope),
            };

            // https://tools.ietf.org/html/rfc7636
            if (Options.UsePkce && Options.ResponseType == OpenIdConnectResponseType.Code)
            {
                var bytes = new byte[32];
                RandomNumberGenerator.Fill(bytes);
                var codeVerifier = Microsoft.AspNetCore.Authentication.Base64UrlTextEncoder.Encode(bytes);

                // Store this for use during the code redemption. See RunAuthorizationCodeReceivedEventAsync.
                properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);

                var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
                var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);

                message.Parameters.Add(OAuthConstants.CodeChallengeKey, codeChallenge);
                message.Parameters.Add(OAuthConstants.CodeChallengeMethodKey, OAuthConstants.CodeChallengeMethodS256);
            }

            var acrValues = properties.GetParameter<string>("acr_values");
            if (!string.IsNullOrWhiteSpace(acrValues)) message.Parameters.Add("acr_values", acrValues);

            // Add the 'max_age' parameter to the authentication request if MaxAge is not null.
            // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
            var maxAge = properties.GetParameter<TimeSpan?>(OpenIdConnectParameterNames.MaxAge) ?? Options.MaxAge;
            if (maxAge.HasValue)
            {
                message.MaxAge = Convert.ToInt64(Math.Floor((maxAge.Value).TotalSeconds))
                    .ToString(CultureInfo.InvariantCulture);
            }

            // Omitting the response_mode parameter when it already corresponds to the default
            // response_mode used for the specified response_type is recommended by the specifications.
            // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
            if (!string.Equals(Options.ResponseType, OpenIdConnectResponseType.Code, StringComparison.Ordinal) ||
                !string.Equals(Options.ResponseMode, OpenIdConnectResponseMode.Query, StringComparison.Ordinal))
            {
                message.ResponseMode = Options.ResponseMode;
            }

            if (Options.ProtocolValidator.RequireNonce)
            {
                message.Nonce = Options.ProtocolValidator.GenerateNonce();
                WriteNonceCookie(message.Nonce);
            }

            Options.CorrelationCookie.Path = Options.IsRealmEnabled ? $"/{_realmStore.Realm}{Options.CallbackPath}" : Options.CallbackPath;
            GenerateCorrelationId(properties);

            var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
            {
                ProtocolMessage = message
            };

            // await Events.RedirectToIdentityProvider(redirectContext);
            if (redirectContext.Handled)
            {
                Logger.RedirectToIdentityProviderHandledResponse();
                return;
            }

            message = redirectContext.ProtocolMessage;

            if (!string.IsNullOrEmpty(message.State))
            {
                properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
            }

            // When redeeming a 'code' for an AccessToken, this value is needed
            properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, message.RedirectUri);

            message.State = Options.StateDataFormat.Protect(properties);

            if (string.IsNullOrEmpty(message.IssuerAddress))
            {
                throw new InvalidOperationException(
                    "Cannot redirect to the authorization endpoint, the configuration may be missing or invalid.");
            }

            await BuildAuthorizationRequest(message, configuration);
            if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
            {
                var redirectUri = message.CreateAuthenticationRequestUrl();
                if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
                {
                    Logger.InvalidAuthenticationRequestUrl(redirectUri);
                }

                Response.Redirect(redirectUri);
                return;
            }
            else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
            {
                var content = message.BuildFormPost();
                var buffer = Encoding.UTF8.GetBytes(content);

                Response.ContentLength = buffer.Length;
                Response.ContentType = "text/html;charset=UTF-8";

                // Emit Cache-Control=no-cache to prevent client caching.
                Response.Headers.CacheControl = "no-cache, no-store";
                Response.Headers.Pragma = "no-cache";
                Response.Headers.Expires = HeaderValueEpocDate;

                await Response.Body.WriteAsync(buffer);
                return;
            }

            throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
        }

        private void WriteNonceCookie(string nonce)
        {
            if (string.IsNullOrEmpty(nonce))
            {
                throw new ArgumentNullException(nameof(nonce));
            }

            Options.NonceCookie.Path = Options.IsRealmEnabled ? $"/{_realmStore.Realm}{Options.CallbackPath}" : Options.CallbackPath;
            var cookieOptions = Options.NonceCookie.Build(Context, Clock.UtcNow);

            Response.Cookies.Append(
                Options.NonceCookie.Name + Options.StringDataFormat.Protect(nonce),
                NonceProperty,
                cookieOptions);
        }

        /// <summary>
        /// Searches <see cref="HttpRequest.Cookies"/> for a matching nonce.
        /// </summary>
        /// <param name="nonce">the nonce that we are looking for.</param>
        /// <returns>echos 'nonce' if a cookie is found that matches, null otherwise.</returns>
        /// <remarks>Examine <see cref="IRequestCookieCollection.Keys"/> of <see cref="HttpRequest.Cookies"/> that start with the prefix: 'OpenIdConnectAuthenticationDefaults.Nonce'.
        /// <see cref="M:ISecureDataFormat{TData}.Unprotect"/> of <see cref="OpenIdConnectOptions.StringDataFormat"/> is used to obtain the actual 'nonce'. If the nonce is found, then <see cref="M:IResponseCookies.Delete"/> of <see cref="HttpResponse.Cookies"/> is called.</remarks>
        private string? ReadNonceCookie(string nonce)
        {
            if (nonce == null)
            {
                return null;
            }

            foreach (var nonceKey in Request.Cookies.Keys)
            {
                if (Options.NonceCookie.Name is string name && nonceKey.StartsWith(name, StringComparison.Ordinal))
                {
                    try
                    {
                        var nonceDecodedValue = Options.StringDataFormat.Unprotect(nonceKey.Substring(Options.NonceCookie.Name.Length, nonceKey.Length - Options.NonceCookie.Name.Length));
                        if (nonceDecodedValue == nonce)
                        {
                            var cookieOptions = Options.NonceCookie.Build(Context, Clock.UtcNow);
                            Response.Cookies.Delete(nonceKey, cookieOptions);
                            return nonce;
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.UnableToProtectNonceCookie(ex);
                    }
                }
            }

            return null;
        }

        private async Task<MessageReceivedContext> RunMessageReceivedEventAsync(OpenIdConnectMessage message, AuthenticationProperties? properties)
        {
            Logger.MessageReceived(message.BuildRedirectUrl());
            var context = new MessageReceivedContext(Context, Scheme, Options, properties)
            {
                ProtocolMessage = message,
            };

            await Events.MessageReceived(context);
            if (context.Result != null)
            {
                if (context.Result.Handled)
                {
                    Logger.MessageReceivedContextHandledResponse();
                }
                else if (context.Result.Skipped)
                {
                    Logger.MessageReceivedContextSkipped();
                }
            }

            return context;
        }

        private async Task<AuthorizationCodeReceivedContext> RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, ClaimsPrincipal? user, AuthenticationProperties properties, JwtSecurityToken? jwt)
        {
            Logger.AuthorizationCodeReceived();

            var tokenEndpointRequest = new OpenIdConnectMessage()
            {
                ClientId = Options.ClientId,
                Code = authorizationResponse.Code,
                GrantType = OpenIdConnectGrantTypes.AuthorizationCode,
                EnableTelemetryParameters = !Options.DisableTelemetry,
                RedirectUri = properties.Items[OpenIdConnectDefaults.RedirectUriForCodePropertiesKey]
            };

            if (Options.ClientAuthenticationType == ClientAuthenticationTypes.CLIENT_SECRET_POST)
                tokenEndpointRequest.ClientSecret = Options.ClientSecret;

            if (Options.ClientAuthenticationType == ClientAuthenticationTypes.PRIVATE_KEY_JWT)
            {
                var descriptor = new SecurityTokenDescriptor
                {
                    Claims = new Dictionary<string, object>
                    {
                        { "sub", Options.ClientId }
                    },
                    Issuer = Options.ClientId,
                    Audience = Options.Authority,
                    SigningCredentials = Options.SigningCredentials
                };
                var handler = new JsonWebTokenHandler();
                var clientAssertion = handler.CreateToken(descriptor);
                tokenEndpointRequest.ClientAssertion = clientAssertion;
                tokenEndpointRequest.ClientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
            }

            // PKCE https://tools.ietf.org/html/rfc7636#section-4.5, see HandleChallengeAsyncInternal
            if (properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier))
            {
                tokenEndpointRequest.Parameters.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
                properties.Items.Remove(OAuthConstants.CodeVerifierKey);
            }

            var backChannelProperty = typeof(AuthorizationCodeReceivedContext).GetProperty("Backchannel");
            var context = new AuthorizationCodeReceivedContext(Context, Scheme, Options, properties)
            {
                ProtocolMessage = authorizationResponse,
                TokenEndpointRequest = tokenEndpointRequest,
                Principal = user,
                JwtSecurityToken = jwt
            };
            backChannelProperty.SetValue(context, Backchannel);
            await Events.AuthorizationCodeReceived(context);
            if (context.Result != null)
            {
                if (context.Result.Handled)
                {
                    Logger.AuthorizationCodeReceivedContextHandledResponse();
                }
                else if (context.Result.Skipped)
                {
                    Logger.AuthorizationCodeReceivedContextSkipped();
                }
            }

            return context;
        }


        private async Task<TokenValidatedContext> RunTokenValidatedEventAsync(OpenIdConnectMessage authorizationResponse, OpenIdConnectMessage? tokenEndpointResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt, string? nonce)
        {
            var context = new TokenValidatedContext(Context, Scheme, Options, user, properties)
            {
                ProtocolMessage = authorizationResponse,
                TokenEndpointResponse = tokenEndpointResponse,
                SecurityToken = jwt,
                Nonce = nonce,
            };

            await Events.TokenValidated(context);
            if (context.Result != null)
            {
                if (context.Result.Handled)
                {
                    Logger.TokenValidatedHandledResponse();
                }
                else if (context.Result.Skipped)
                {
                    Logger.TokenValidatedSkipped();
                }
            }

            return context;
        }

        private async Task<TokenResponseReceivedContext> RunTokenResponseReceivedEventAsync(
            OpenIdConnectMessage message,
            OpenIdConnectMessage tokenEndpointResponse,
            ClaimsPrincipal user,
            AuthenticationProperties properties)
        {
            Logger.TokenResponseReceived();
            var context = new TokenResponseReceivedContext(Context, Scheme, Options, user, properties)
            {
                ProtocolMessage = message,
                TokenEndpointResponse = tokenEndpointResponse,
            };

            await Events.TokenResponseReceived(context);
            if (context.Result != null)
            {
                if (context.Result.Handled)
                {
                    Logger.TokenResponseReceivedHandledResponse();
                }
                else if (context.Result.Skipped)
                {
                    Logger.TokenResponseReceivedSkipped();
                }
            }

            return context;
        }

        private async Task<UserInformationReceivedContext> RunUserInformationReceivedEventAsync(ClaimsPrincipal principal, AuthenticationProperties properties, OpenIdConnectMessage message, JsonDocument user)
        {
            Logger.UserInformationReceived(user.ToString()!);

            var context = new UserInformationReceivedContext(Context, Scheme, Options, principal, properties)
            {
                ProtocolMessage = message,
                User = user,
            };

            await Events.UserInformationReceived(context);
            if (context.Result != null)
            {
                if (context.Result.Handled)
                {
                    Logger.UserInformationReceivedHandledResponse();
                }
                else if (context.Result.Skipped)
                {
                    Logger.UserInformationReceivedSkipped();
                }
            }

            return context;
        }


        private async Task<AuthenticationFailedContext> RunAuthenticationFailedEventAsync(OpenIdConnectMessage message, Exception exception)
        {
            var context = new AuthenticationFailedContext(Context, Scheme, Options)
            {
                ProtocolMessage = message,
                Exception = exception
            };

            await Events.AuthenticationFailed(context);
            if (context.Result != null)
            {
                if (context.Result.Handled)
                {
                    Logger.AuthenticationFailedContextHandledResponse();
                }
                else if (context.Result.Skipped)
                {
                    Logger.AuthenticationFailedContextSkipped();
                }
            }

            return context;
        }

        // Note this modifies properties if Options.UseTokenLifetime
        private ClaimsPrincipal ValidateToken(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters, OpenIdConnectConfiguration configuration, out JwtSecurityToken jwt)
        {
            if (!Options.SecurityTokenValidator.CanReadToken(idToken))
            {
                Logger.UnableToReadIdToken(idToken);
                throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken));
            }

            if (configuration != null)
            {
                var issuer = new[] { configuration.Issuer };
                validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuer) ?? issuer;

                validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(configuration.SigningKeys)
                    ?? configuration.SigningKeys;
            }

            var principal = Options.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out SecurityToken validatedToken);
            if (validatedToken is JwtSecurityToken validatedJwt)
            {
                jwt = validatedJwt;
            }
            else
            {
                Logger.InvalidSecurityTokenType(validatedToken?.GetType().ToString());
                throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJwt, validatedToken?.GetType()));
            }

            if (validatedToken == null)
            {
                Logger.UnableToValidateIdToken(idToken);
                throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateToken, idToken));
            }

            if (Options.UseTokenLifetime)
            {
                var issued = validatedToken.ValidFrom;
                if (issued != DateTime.MinValue)
                {
                    properties.IssuedUtc = issued;
                }

                var expires = validatedToken.ValidTo;
                if (expires != DateTime.MinValue)
                {
                    properties.ExpiresUtc = expires;
                }
            }

            return principal;
        }

        private async Task BuildAuthorizationRequest(OpenIdConnectMessage message, OpenIdConnectConfiguration configuration)
        {
            if (Options.RequestType == RequestTypes.NONE) return;
            switch (Options.RequestType)
            {
                case RequestTypes.REQUEST:
                case RequestTypes.PAR:
                    var claims = message.Parameters.ToDictionary(kvp => kvp.Key, kvp => (object)kvp.Value);
                    var clientId = message.ClientId;
                    var descriptor = new SecurityTokenDescriptor
                    {
                        Claims = claims,
                        Issuer = clientId,
                        SigningCredentials = Options.SigningCredentials
                    };
                    var handler = new JsonWebTokenHandler();
                    var request = handler.CreateToken(descriptor);
                    if (Options.RequestType == RequestTypes.REQUEST)
                    {
                        message.Parameters.Clear();
                        message.ClientId = clientId;
                        message.Parameters.Add(RequestParameterName, request);
                        return;
                    }

                    var parUrl = configuration.AdditionalData[PushedAuthorizationRequestEndpoint].ToString();
                    var dic = new Dictionary<string, string>
                    {
                        { RequestParameterName, request },
                        { ClientIdParameterName, clientId }
                    };
                    var httpRequest = new HttpRequestMessage
                    {
                        Method = HttpMethod.Post,
                        RequestUri = new Uri(parUrl),
                        Content = new FormUrlEncodedContent(dic)
                    };
                    var httpResult = await Backchannel.SendAsync(httpRequest);
                    httpResult.EnsureSuccessStatusCode();
                    var json = await httpResult.Content.ReadFromJsonAsync<PARResponse>();
                    message.Parameters.Clear();
                    message.ClientId = clientId;
                    message.Parameters.Add(RequestUriParameterName, json.RequestUri);
                    return;
            }

            throw new NotImplementedException($"Request {Options.RequestType} is not yet implemented");
        }

        private void CheckOptions(OpenIdConnectConfiguration configuration)
        {
            if ((Options.RequestType == RequestTypes.REQUEST || Options.RequestType == RequestTypes.PAR) && Options.SigningCredentials == null)
                throw new InvalidOperationException("SigningCredentials is required when request type is equals to request");

            if (!configuration.AdditionalData.ContainsKey(PushedAuthorizationRequestEndpoint) && Options.RequestType == RequestTypes.PAR)
                throw new InvalidOperationException("Identity Server doesn't support PAR request");

            if (configuration.AdditionalData.ContainsKey(RequirePushedAuthorizationRequests) && bool.TryParse(configuration.AdditionalData[RequirePushedAuthorizationRequests].ToString(), out bool requiredPushedAuthorizationRequests) && requiredPushedAuthorizationRequests && Options.RequestType != RequestTypes.PAR)
                throw new InvalidOperationException("Pushed Authorization Request must be used");

            if (Options.ClientAuthenticationType == ClientAuthenticationTypes.CLIENT_SECRET_POST && string.IsNullOrWhiteSpace(Options.ClientSecret))
                throw new InvalidOperationException("The client_secret_post authentication method cannot be used because the client secret is missing");

            if (Options.ClientAuthenticationType == ClientAuthenticationTypes.TLS_CLIENT_AUTH && Options.MTLSCertificate == null)
                throw new InvalidOperationException("The tls_client_auth authentication method cannot be used because the certificate is missing");

            if (Options.ClientAuthenticationType == ClientAuthenticationTypes.PRIVATE_KEY_JWT && Options.SigningCredentials == null)
                throw new InvalidOperationException("The private_key_jwt authentication method cannot be used beacuse the JWK is missing");
        }

        private class PARResponse
        {
            [JsonPropertyName(RequestUriParameterName)]
            public string RequestUri { get; set; }
        }

        private void PopulateSessionProperties(OpenIdConnectMessage message, AuthenticationProperties properties, OpenIdConnectConfiguration configuration)
        {
            if (!string.IsNullOrEmpty(message.SessionState))
            {
                properties.Items[OpenIdConnectSessionProperties.SessionState] = message.SessionState;
            }

            if (!string.IsNullOrEmpty(configuration?.CheckSessionIframe))
            {
                properties.Items[OpenIdConnectSessionProperties.CheckSessionIFrame] = configuration.CheckSessionIframe;
            }
        }

        /// <summary>
        /// Redeems the authorization code for tokens at the token endpoint.
        /// </summary>
        /// <param name="tokenEndpointRequest">The request that will be sent to the token endpoint and is available for customization.</param>
        /// <returns>OpenIdConnect message that has tokens inside it.</returns>
        protected virtual async Task<OpenIdConnectMessage> RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest, string dpopNonce = null, OpenIdConnectConfiguration configuration = null)
        {
            Logger.RedeemingCodeForTokens();
            var requestMessage = new HttpRequestMessage(HttpMethod.Post, tokenEndpointRequest.TokenEndpoint ?? configuration?.TokenEndpoint);
            requestMessage.Content = new FormUrlEncodedContent(tokenEndpointRequest.Parameters);
            HttpResponseMessage responseMessage = null;
            switch (Options.ClientAuthenticationType)
            {
                case ClientAuthenticationTypes.CLIENT_SECRET_POST:
                case ClientAuthenticationTypes.PRIVATE_KEY_JWT:
                    requestMessage.Version = Backchannel.DefaultRequestVersion;
                    if (Options.IsDPoPUsed) responseMessage = await RequestTokenWithDPoP();
                    else responseMessage = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
                    break;
                case ClientAuthenticationTypes.TLS_CLIENT_AUTH:
                    var mtlsEndpointAliases = configuration.AdditionalData[MtlsEndpointAliasesName];
                    var type = mtlsEndpointAliases.GetType();
                    var getValueMethod = type.GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public).First(m => m.Name == "GetValue" && m.GetParameters().Length == 1);
                    var val = getValueMethod.Invoke(mtlsEndpointAliases, new object[] { TokenEndpointParameterName });
                    var tokenEdp = val.ToString();
                    var handler = new HttpClientHandler
                    {
                        ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; },
                        CheckCertificateRevocationList = false,
                        ClientCertificateOptions = ClientCertificateOption.Manual,
                        SslProtocols = SslProtocols.Tls12
                    };
                    handler.ClientCertificates.Add(Options.MTLSCertificate);
                    using (var httpClient = new HttpClient(handler))
                    {
                        responseMessage = await httpClient.SendAsync(requestMessage, Context.RequestAborted);
                    }
                    break;
                default:
                    throw new InvalidOperationException($"The client authentication type {Options.ClientAuthenticationType} is not supported");
            }

            var contentMediaType = responseMessage.Content.Headers.ContentType?.MediaType;
            if (string.IsNullOrEmpty(contentMediaType))
            {
                Logger.LogDebug($"Unexpected token response format. Status Code: {(int)responseMessage.StatusCode}. Content-Type header is missing.");
            }
            else if (!string.Equals(contentMediaType, "application/json", StringComparison.OrdinalIgnoreCase))
            {
                Logger.LogDebug($"Unexpected token response format. Status Code: {(int)responseMessage.StatusCode}. Content-Type {responseMessage.Content.Headers.ContentType}.");
            }

            // Error handling:
            // 1. If the response body can't be parsed as json, throws.
            // 2. If the response's status code is not in 2XX range, throw OpenIdConnectProtocolException. If the body is correct parsed,
            //    pass the error information from body to the exception.
            OpenIdConnectMessage message;
            try
            {
                var responseContent = await responseMessage.Content.ReadAsStringAsync(Context.RequestAborted);
                message = new OpenIdConnectMessage(responseContent);
            }
            catch (Exception ex)
            {
                throw new OpenIdConnectProtocolException($"Failed to parse token response body as JSON. Status Code: {(int)responseMessage.StatusCode}. Content-Type: {responseMessage.Content.Headers.ContentType}", ex);
            }

            if (!responseMessage.IsSuccessStatusCode)
            {
                if (message.Error == "use_dpop_nonce") return await RedeemAuthorizationCodeAsync(tokenEndpointRequest, responseMessage.Headers.First(h => h.Key == "DPoP-Nonce").Value.First(), configuration);
                throw CreateOpenIdConnectProtocolException(message, responseMessage);
            }

            return message;

            async Task<HttpResponseMessage> RequestTokenWithDPoP()
            {
                RotateDPoPSecurityKey();
                var handler = new DPoPHandler();
                var dpop = handler.Create("POST", configuration.TokenEndpoint, _dPoPSecurityKey, SecurityAlgorithms.EcdsaSha256, dpopNonce);
                requestMessage.Headers.Add("DPoP", dpop.Token);
                return await Backchannel.SendAsync(requestMessage, Context.RequestAborted);
            }
        }

        private void RotateDPoPSecurityKey()
        {
            if (_lastDPoPSecurityKeyRotationDateTime == null || _lastDPoPSecurityKeyRotationDateTime.Value.AddSeconds(Options.DPoPSecurityKeyRotationInSeconds) >= DateTime.UtcNow)
            {
                _dPoPSecurityKey = new ECDsaSecurityKey(ECDsa.Create(ECCurve.NamedCurves.nistP256));
                _lastDPoPSecurityKeyRotationDateTime = DateTime.UtcNow;
            }
        }

        /// <summary>
        /// Build a redirect path if the given path is a relative path.
        /// </summary>
        private string BuildRedirectUriIfRelative(string uri)
        {
            if (string.IsNullOrEmpty(uri))
            {
                return uri;
            }

            if (!uri.StartsWith('/'))
            {
                return uri;
            }

            return BuildRedirectUri(uri);
        }

        private async Task<OpenIdConnectConfiguration> GetConfiguration()
        {
            if (this.Options.IsRealmEnabled)
            {
                var realm = string.IsNullOrWhiteSpace(_realmStore.Realm) ? "master" : _realmStore.Realm;
                if (!RealmConfigurationManagers.ContainsKey(realm))
                {
                    var metadataAdr = Options.Authority;
                    if (!metadataAdr.EndsWith('/'))
                        metadataAdr += "/";
                    metadataAdr += $"{realm}/.well-known/openid-configuration";
                    var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataAdr, new OpenIdConnectConfigurationRetriever(), new HttpDocumentRetriever(Options.Backchannel)
                    {
                        RequireHttps = Options.RequireHttpsMetadata
                    })
                    {
                        RefreshInterval = Options.RefreshInterval,
                        AutomaticRefreshInterval = Options.AutomaticRefreshInterval
                    };
                    RealmConfigurationManagers.Add(realm, configurationManager);
                }

                if (!RealmOpenidConfigurations.ContainsKey(realm))
                {
                    var configuration = await RealmConfigurationManagers[realm].GetConfigurationAsync(Context.RequestAborted);
                    RealmOpenidConfigurations.Add(realm, configuration);
                }

                return RealmOpenidConfigurations[realm];
            }

            if (_configuration == null && Options.ConfigurationManager != null)
            {
                Logger.UpdatingConfiguration();
                _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
            }

            return _configuration;
        }

        private OpenIdConnectProtocolException CreateOpenIdConnectProtocolException(OpenIdConnectMessage message, HttpResponseMessage? response)
        {
            var description = message.ErrorDescription ?? "error_description is null";
            var errorUri = message.ErrorUri ?? "error_uri is null";

            if (response != null)
            {
                Logger.ResponseErrorWithStatusCode(message.Error, description, errorUri, (int)response.StatusCode);
            }
            else
            {
                Logger.ResponseError(message.Error, description, errorUri);
            }

            var ex = new OpenIdConnectProtocolException(string.Format(
                CultureInfo.InvariantCulture,
                Resources.MessageContainsError,
                message.Error,
                description,
                errorUri));
            ex.Data["error"] = message.Error;
            ex.Data["error_description"] = description;
            ex.Data["error_uri"] = errorUri;
            return ex;
        }
    }
}